16.4 回收
内存回收的源头是垃圾清理操作。
之所以说回收而非释放,是因为整个内存分配器的核心是内存复用,不再使用的内存会被放回合适位置,等下次分配时再次使用。只有当空闲内存资源过多时,才会考虑释放。
基于效率考虑,回收操作自然不会直接盯着单个对象,而是以span为基本单位。通过比对bitmap里的扫描标记,逐步将object收归原span,最终上交central或heap复用。
清理函数sweepone调用mSpan_Sweep来引发内存分配器回收流程。
mgcsweep.go
func sweepone()uintptr{ if!mSpan_Sweep(s,false) { … } }
func mSpan_Sweep(s*mspan,preserve bool)bool{ var head,end gclinkptr
// 为span中的空闲object设置标记,无须再次扫描 for link:=s.freelist;link.ptr() !=nil;link=link.ptr().next{ heapBitsForAddr(uintptr(link)).setMarkedNonAtomic() }
// 遍历span,收集未标记的不可达object(不包括freelist,它们已被标记) heapBitsSweepSpan(s.base(),size,n,func(p uintptr) { if cl==0{ // 大对象:重置bitmap,更新sweepgen heapBitsForSpan(p).initSpan(s.layout()) atomicstore(&s.sweepgen,sweepgen) freeToHeap=true }else{ // 使用head、end构建链表,收集不可达object if head.ptr() ==nil{ head=gclinkptr(p) }else{ end.ptr().next=gclinkptr(p) } end=gclinkptr(p) end.ptr().next=gclinkptr(0x0bade5)
// 收集计数
nfree++
}
})
// 回收内存 // 小对象:如果没有可回收object,那么维持原状态,根本无须处理 // 大对象:整个span就是一个object,直接交还heap if nfree>0{ mCentral_FreeSpan(&mheap_.central[cl].mcentral,s,int32(nfree),head,end, …) }else if freeToHeap{ mHeap_Free(&mheap_,s,1) } }
遍历span,将收集到的不可达object合并到freelist链表。如该span已收回全部object,那么就将这块完全自由的内存还给heap,以便后续复用。
mcentral.go
func mCentral_FreeSpan(cmcentral,smspan,n int32,start,end gclinkptr…)bool{ // 判断span是否为空(没有空闲object) wasempty:=s.freelist.ptr() ==nil
// 将收集到链表合并到freelist end.ptr().next=s.freelist s.freelist=start s.ref-=uint16(n)
// 阻止进一步回收 if preserve{ atomicstore(&s.sweepgen,mheap_.sweepgen) return false }
// 将原本为空的span转移到central.nonempty链表 if wasempty{ mSpanList_Remove(s) mSpanList_Insert(&c.nonempty,s) }
// 如果还有object被使用,那么终止 if s.ref!=0{ return false }
// 如果收回全部object,就从central交还给heap mSpanList_Remove(s) heapBitsForSpan(s.base()).initSpan(s.layout()) mHeap_Free(&mheap_,s,0)
return true }
无论是向操作系统申请内存,还是清理回收内存,只要往heap里放span,都会尝试合并左右相邻的闲置span,以构成更大的自由块。
mheap.go
func mHeap_Free(hmheap,smspan,acct int32) { systemstack(func() { mHeap_FreeSpanLocked(h,s,true,true,0) }) }
func mHeap_FreeSpanLocked(hmheap,smspan,acctinuse,acctidle bool,unusedsince int64) { // 从现有链表移除 mSpanList_Remove(s)
// 计算偏移量 p:=uintptr(s.start) p-=uintptr(unsafe.Pointer(h.arena_start)) >> _PageShift
if p>0{ // 通过spans数组访问左侧相邻span t:=h_spans[p-1]
// 检查合并条件
if t!=nil&&t.state!= _MSpanInUse&&t.state!= _MSpanStack{
// 合并,更新属性
s.start=t.start
s.npages+=t.npages
// 更新spans里的信息
p-=t.npages
h_spans[p] =s
// 释放原左侧span对象
mSpanList_Remove(t)
fixAlloc_Free(&h.spanalloc, (unsafe.Pointer)(t))
}
}
// 检查右侧span if(p+s.npages)*ptrSize<h.spans_mapped{ t:=h_spans[p+s.npages] if t!=nil&&t.state!= _MSpanInUse&&t.state!= _MSpanStack{ // 合并右侧span,更新属性 s.npages+=t.npages
// 更新spans信息
h_spans[p+s.npages-1] =s
// 释放原右侧span对象
mSpanList_Remove(t)
fixAlloc_Free(&h.spanalloc, (unsafe.Pointer)(t))
}
}
// 根据页数插入free/freelarge链表 if s.npages<uintptr(len(h.free)) { mSpanList_Insert(&h.free[s.npages],s) }else{ mSpanList_Insert(&h.freelarge,s) } }
回收操作至此结束。这些被收回的span并不会被释放,而是等待复用。